<?php
require_once 'Zend/Rest/Client.php';
require_once 'Cafepress/Service/Exception.php';
require_once 'Cafepress/Service/Result.php';
require_once 'Cafepress/Service/Action.php';

/**
 * @category Cafepress
 * @package	Cafepress
 */
class Cafepress_Service extends Zend_Rest_Client
{
	
	/**
	 * The user token
	 * 
	 * @var string
	 */
	private $_userToken;
	
	/**
	 * String that defines the id of the method cache
	 *
	 * @var string
	 */
	const ID_METHODCACHE = 'Cafepress_Service_MethodCache';
	
	/**
	 * The server name
	 *
	 * @var string
	 */
	private $_server = 'http://open-api.cafepress.com';
	
	/**
	 * The API Key
	 *
	 * @var string
	 */
	private $_apiKey = "";
	
	/**
	 * The service cache, accessible via the setCache method. The service
	 * will attempt to cache the authenticated API key for Cafepress.
	 * 
	 * @var Zend_Cache_Core
	 */
	private $_cache;
	
	/**
	 * The method map pulled from the service
	 *
	 * @var string
	 */
	private $_methodMap;
	
	/**
	 * The API Version
	 *
	 * @var string
	 */
	private $_version = '3';
	
	/**
	 * The Login
	 * 
	 * @var string
	 */
	private $_login;
	
	/**
	 * The Password
	 * 
	 * @var string
	 */
	private $_password;
	
	/**
	 * The current resource name
	 *
	 * @var string
	 */
	private $_currentResource;
	
	/**
	 * Constructor
	 * 
	 * @param string
	 */							
	public function __construct ( $apiKey ) 
	{
		$this->_apiKey = $apiKey;
		$this->setUri( $this->_server );
	}
	
	/**
	 * Returns dynamically generated resource.
	 *
	 * @param string $name
	 */
	public function __get ( $name )
	{
		// First pull the method mapping
		$map = $this->getMethodMap();
		
		if ( empty ( $map[$name] ) )
		{
			throw new Cafepress_Service_Exception('Invalid resource "' . $name . '"');
			return null;
		}
		
		$this->_currentResource = $name;
		return $this;
	}
	
	/**
	 * Processes the methods invoked.
	 *
	 * @param string $methodName
	 * @param array $params
	 */
	public function __call ( $methodName, $params = array() )
	{
		// First pull the method mapping
		$map = $this->getMethodMap();
		
		// Now check to see if we're a self invocation or not.
		if ( empty ( $this->_currentResource ) || empty($map[$this->_currentResource][$methodName]))
		{
			throw new Cafepress_Service_Exception('Invalid method "' . $methodName . '"');
			return;
		}
		
		
		// Pull the action spec from the cache
		$action = $map[$this->_currentResource][$methodName];
		
		// Convert the parameter array into an associative array and add the API Key
		$params = $this->parameterCompositionHelper( $action, $params );
		
		// Construct the URL from the passed parameters.
		$uri = $this->uriCompositionHelper( $action );
		
		// Send the response
		$response = $this->restGet( $uri, $params );
		$result = new Cafepress_Service_Result();
		
		if ( $response->getStatus() !== 200 )
		{
			$result->success = false;
			$result->result = $response->getBody();
		}
		else
		{
			$result->success = true;
			$result->result = new SimpleXMLElement( $response->getBody() );
		}
		
		$this->_currentResource = null;
		
		return $result;
	}
	
	/**
	 * Composits the parameter array into an associative array according
	 * to the order received by the method specification.
	 *
	 * @param array $actionSpec
	 * @param array $parameters
	 * @return array
	 */
	private function parameterCompositionHelper ( $actionSpec, $parameters )
	{
		$assocParams = array();
		$paramCount = count ( $actionSpec->params );
		$i = 0;
	
		foreach ( $actionSpec->params as $key => $value )
		{
			switch($key)
			{
				case 'appKey':
					$assocParams[$key] = $this->_apiKey;
					break;
				case 'email':
					$assocParams[$key] = $this->_login;
					break;
				case 'password':
					$assocParams[$key] = $this->_password;
					break;
				case 'userToken':
					$assocParams[$key] = $this->loadUserToken();
					break;
				default:
					if ( isset ( $parameters[$i] ) )
					{
						$assocParams[$key] = $parameters[$i];
					}
			
					$i++;
			}
		}
		
		$assocParams['v'] = $this->_version;
		
		// For good measure, attach any usertoken we might have if it happens
		// to be set. This is a catchall for calls that don't explicitly declare
		// that a usertoken is required in the methodspec.
		if ( isset($this->_userToken) )
		{
			$assocParams['userToken'] = $this->_userToken;
		}
		
		return $assocParams;
	}
	
	/**
	 * Uses a method specification item and a passed set of parameters
	 * to compose the invocation uri for the service.
	 *
	 * @param array $methodSpec
	 * @return string
	 */
	private function uriCompositionHelper ( $actionSpec )
	{
		$url = "/{$this->_currentResource}.{$actionSpec->name}.cp";
		return $url;
	}
	
	/**
	 * Loads the method map from the cache
	 *
	 * @return array
	 */
	private function getMethodMap ()
	{
		// Return the method map if we already have it.
		if ( !isset ( $this->_methodMap ) )
		{
			// Try to get it from the cache
			if ( $this->_cache->test(Cafepress_Service::ID_METHODCACHE) )
			{
				// Reconstruct the method table.
				$this->_methodMap = $this->_cache->load(Cafepress_Service::ID_METHODCACHE);
			}
			else
			{
				// Create the methodtable collection
				$methodTable = array();
				
				// Pull the method table response
				$path = "/documentation.list.cp";
				$params = array('appKey' => $this->_apiKey, 'v' => $this->_version );
				$response = $this->restGet( $path, $params );
				
				// Retrieve and parse the body
				$body = $response->getBody();
				$xml = new SimpleXMLElement($body);
				
				// Iterate through the resources
				$resLen = count($xml->resource);
				for ( $i = 0; $i < $resLen; $i++ )
				{
					$resource = $xml->resource[$i];
					$resourceName = (String)$resource['name'];
					
					$methodTable[$resourceName] = array();
					
					$actionLen = count($resource->action);
					for ( $j = 0; $j < $actionLen; $j++ )
					{
						$actionXml = $resource->action[$j];
						$action = Cafepress_Service_Action::fromXml( $actionXml );
						$methodTable[$resourceName][$action->name] = $action;
					}
				}
				
				$this->_methodMap = $methodTable;
				
				$this->_cache->save($this->_methodMap, Cafepress_Service::ID_METHODCACHE);
			}
		}
		
		return $this->_methodMap;
	}
	
	
	/**
	 * Sets the API Key
	 *
	 * @param string $key
	 */
	public function setApiKey( $key )
	{
		$this->_apiKey = $key;
	}
	
	/**
	 * Returns the API Key
	 *
	 * @return string
	 */
	public function getApiKey ()
	{
		return $this->_apiKey;
	}
	/**
	 * This method sets the login for the user.
	 * 
	 * @param string $login
	 */
	public function setLogin ( $login )
	{
		$this->_login = $login;
	}
	
	/**
	 * Returns the login email
	 * 
	 * @return string
	 */
	public function getLogin ( )
	{
		return $this->_login;
	}
	
	/**
	 * This method sets the password for the user.
	 * 
	 * @param string $password
	 */
	public function setPassword ( $password )
	{
		$this->_password = $password;
	}
	
	/**
	 * Returns the login password
	 * 
	 * @return string
	 */
	public function getPassword ( )
	{
		return $this->_password;
	}

	/**
	 * Sets the API Version
	 *
	 * @param string $version
	 */
	public function setVersion ( $version = '3' )
	{
		switch ( $version )
		{
			case '3':
			default:
				$this->_version = $version;
		}
	}
	
	/**
	 * Returns the version
	 *
	 * @return string
	 */
	public function getVersion ()
	{
		return $this->_version;
	}
	
	/**
	 * This method sets the cache
	 * 
	 * @param Zend_Cache_Core $cache
	 */
	public function setCache ( $cache )
	{
		$this->_cache = $cache;
	}
	
	/**
	 * Returns the cache
	 * 
	 * @return Zend_Cache_Core
	 */
	public function getCache ( )
	{
		return $this->_cache;
	}
	
	/**
	 * This private method retrieves the current user token.
	 * 
	 * @param string $token
	 */
	public function loadUserToken ( )
	{
		// We only allow valid user tokens to be saved to the class instance.
		if ( isset($this->_userToken) )
		{
			return $this->_userToken;
		}
		else
		{
			// We still need a user token. Make sure we have credentials
			if ( !isset($this->_login) || !isset($this->_password))
			{
				throw new InvalidArgumentException('Invalid authentication parameters provided', 500);
				return;
			}
			
			// We have two options now. Load from the cache and verify that token,
			// or simply call login again. Either way we have to preserve the
			// resource ID of the previous invocation.
			$previousResource = $this->_currentResource;
			$cacheKey = md5( $this->_login . $this->_password );
			
			if ( $this->_cache->test($cacheKey))
			{
				$testUserToken = $this->_cache->load($cacheKey);
				$response = $this->authentication->isValid($testUserToken);
				
				// If the response is valid, save the token inside of our happy
				// little service.
				if ( $response->success && (string)$response->result == 'True')
				{
					$this->_userToken = $testUserToken;
				}
			}
			
			// Alright, so we tried to validate any cached key we might have. If
			// we STILL don't have a user token, try to login.
			if ( !isset($this->_userToken ) )
			{
				// Try to get a user token.
				$response = $this->authentication->getUserToken();
				
				if ( $response->success )
				{
					// Save the token.
					$this->_userToken = (string)$response->result;
					$this->_cache->save($this->_userToken, $cacheKey,array(),1800);
				}
				else
				{
					// Login failed. Throw an error.
					throw new InvalidArgumentException('Invalid authentication parameters provided', 500);
					return;
				}
			}
			
			$this->_currentResource = $previousResource;
		}
		
		return $this->_userToken;
	}
}